<?php

namespace App\Services\QCloud;

use App\Support\Time;
use Exception;

class TLSSig
{
    private $privateKey;
    private $publicKey;
    
    private $appId;
    private $keyDir;
    
    public function __construct($appId, $keyDir)
    {
        $this->appId = $appId;
        
        $this->keyDir = $keyDir;
    }
    
    /**
     * 获取私钥，如果要生成 user sig，则需要私钥
     *
     * @return string
     *
     * @throws Exception
     */
    public function getPrivateKey()
    {
        if (isset($this->privateKey)) {
            return $this->privateKey;
        }
        
        $content = file_get_contents("{$this->keyDir}/private_key");
        $this->privateKey = openssl_pkey_get_private($content);
        
        if (false === $this->privateKey) {
            throw new Exception(openssl_error_string());
        }
        
        return $this->privateKey;
    }
    
    /**
     * 获取公钥，如果要验证 user sig，则需要公钥
     *
     * @return string
     *
     * @throws Exception
     */
    public function getPublicKey()
    {
        if (isset($this->publicKey)) {
            return $this->publicKey;
        }
        
        $content = file_get_contents("{$this->keyDir}/public_key");
        $this->publicKey = openssl_pkey_get_public($content);
        
        if (false === $this->publicKey) {
            throw new Exception(openssl_error_string());
        }
        
        return $this->publicKey;
    }
    
    /**
     * 用于url的 base64encode
     *
     * @param string $string 需要编码的数据
     * @return string 编码后的base64串
     *
     * @throws Exception
     */
    private function base64Encode($string)
    {
        $replace = ['+' => '*', '/' => '-', '=' => '_'];
        
        $base64 = base64_encode($string);
        if (false === $base64) {
            throw new Exception('base64_encode error');
        }
        
        return str_replace(array_keys($replace), array_values($replace), $base64);
    }
    
    /**
     * 用于url的 base64decode
     *
     * @param string $base64 需要解码的base64串
     * @return string 解码后的数据
     *
     * @throws Exception
     */
    private function base64Decode($base64)
    {
        $replace = ['+' => '*', '/' => '-', '=' => '_'];
        
        $string = str_replace(array_values($replace), array_keys($replace), $base64);
        $result = base64_decode($string);
        
        if ($result == false) {
            throw new Exception('base64_decode error');
        }
        
        return $result;
    }
    
    /**
     * 根据json内容生成需要签名的buf串
     *
     * @param array $array
     * @return string 按标准格式生成的用于签名的字符串
     *
     * @throws Exception
     */
    private function genSignContent(array $array)
    {
        $members = [
            'TLS.appid_at_3rd',
            'TLS.account_type',
            'TLS.identifier',
            'TLS.sdk_appid',
            'TLS.time',
            'TLS.expire_after'
        ];
        
        $content = '';
        foreach ($members as $member) {
            if (!isset($array[$member])) {
                throw new Exception('json need ' . $member);
            }
            
            $content .= "{$member}:{$array[$member]}\n";
        }
        
        return $content;
    }
    
    /**
     * ECDSA-SHA256 签名
     *
     * @param string $data 需要签名的数据
     * @return string
     *
     * @throws Exception
     */
    private function sign($data)
    {
        $signature = '';
        if (! openssl_sign($data, $signature, $this->getPrivateKey(), 'sha256')) {
            throw new Exception(openssl_error_string());
        }
        return $signature;
    }
    
    /**
     * 验证 ECDSA-SHA256 签名
     *
     * @param string $data 需要验证的数据原文
     * @param string $sig 需要验证的签名
     * @return int
     *
     * @throws Exception
     */
    private function verify($data, $sig)
    {
        $ret = openssl_verify($data, $sig, $this->getPublicKey(), 'sha256');
        
        if ($ret == -1) {
            throw new Exception(openssl_error_string());
        }
        
        return $ret;
    }
    
    /**
     * 生成 user sig
     *
     * @param string $identifier 用户名
     * @param int|float $expire user sig 有效期，默认为365天
     * @return string 生成的 UserSig
     *
     * @throws Exception
     */
    public function genSig($identifier, $expire = Time::SECONDS_OF_YEAR)
    {
        $array = [
            'TLS.account_type' => '0',
            'TLS.identifier' => (string) $identifier,
            'TLS.appid_at_3rd' => '0',
            'TLS.sdk_appid' => (string) $this->appId,
            'TLS.expire_after' => (string) $expire,
            'TLS.version' => '201512300000',
            'TLS.time' => (string) time()
        ];
        
        $content = $this->genSignContent($array);
        $signature = $this->sign($content);
        
        $array['TLS.sig'] = base64_encode($signature);
        if (false == $array['TLS.sig']) {
            throw new Exception('base64_encode error');
        }
        
        $json = json_encode($array);
        if ($json === false) {
            throw new Exception('json_encode error');
        }
        
        $compressed = gzcompress($json);
        if ($compressed === false) {
            throw new Exception('gzcompress error');
        }
        
        return $this->base64Encode($compressed);
    }

    /**
     * 验证 user sig
     *
     * @param string $sig user sig
     * @param string $identifier 需要验证用户名
     * @param int $initTime user sig 中的生成时间
     * @param int $expireTime user sig 中的有效期 如：3600秒
     * @param string $error 失败时的错误信息
     * @return bool 验证是否成功
     */
    public function verifySig($sig, $identifier, &$initTime, &$expireTime, &$error)
    {
        try {
            $error = '';
            $decodedSig = $this->base64Decode($sig);
            $json = gzuncompress($decodedSig);
            if (false === $json) {
                throw new Exception('gzuncompress error');
            }
            
            $array = json_decode($json, true);
            if (false == $array) {
                throw new Exception('json_decode error');
            }
            
            if ($array['TLS.identifier'] !== $identifier) {
                throw new Exception("identifier error sigid:{$array['TLS.identifier']} id:{$identifier}");
            }
            if ($array['TLS.sdk_appid'] != $this->appId) {
                throw new Exception("appid error sigappid:{$array['TLS.appid']} thisappid:{$this->appId}");
            }
            
            $content = $this->genSignContent($array);
            $signature = base64_decode($array['TLS.sig']);
            if ($signature == false) {
                throw new Exception('sig json_decode error');
            }
            
            if (! $this->verify($content, $signature)) {
                throw new Exception('verify failed');
            }
            
            $initTime = $array['TLS.time'];
            $expireTime = $array['TLS.expire_after'];
            
            return true;
        } catch (Exception $ex) {
            $error = $ex->getMessage();
            
            return false;
        }
    }
}
